//
// Implementor.java
//

package com.photoexpert;

import com.drew.imaging.ImageMetadataReader;
import com.drew.imaging.ImageProcessingException;
import com.drew.metadata.Directory;
import com.drew.metadata.Metadata;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.RenderingHints;
import java.awt.Transparency;
import java.awt.image.BufferedImage;
import java.awt.image.BufferedImageOp;
import java.awt.image.ConvolveOp;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashSet;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;
import javax.imageio.ImageReader;
import javax.imageio.stream.ImageInputStream;
import javax.swing.JOptionPane;

class Implementor {

    static Implementor Instance() {
        if(_instance == null) {
            _instance = new Implementor();
        }
        return _instance;
    }

    void setPresentor(Presentor p) { _presentor = p; }

    BufferedImage getImage() { return _image; }

    void setImage(BufferedImage newImg) {
        backup();
        _image = newImg;
        _width = newImg.getWidth();
        _height = newImg.getHeight();
        _transparent = newImg.getTransparency() != Transparency.OPAQUE;
        _presentor.refreshImage();
    }

    int getHeight() { return _height; }

    int getWidth() { return _width; }

    boolean hasTransparency() { return _transparent; }

    void newImage(int width, int height, boolean transparent) {
        _logger.log(Level.INFO, "Creating image width={0} height={1} transparent={2}",
            new Object[]{width, height, transparent});
        _width = width;
        _height = height;
        _transparent = transparent;
        int imgType = _transparent ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB;
        _image = new BufferedImage(width, height, imgType);
        int value = _transparent ? 0x00000000 : 0x00ffffff;
        for(int x = 0; x < _width; ++x)
            for(int y = 0; y < _height; ++y)
                _image.setRGB(x, y, value);
        _presentor.refreshImage();
        App.Instance().fileOpened(null);
    }

    void openFile(File f) {
        try {
            String fileName = f.getAbsolutePath();
            _logger.log(Level.INFO, "Opening file {0}", fileName);
            FileInputStream fis = new FileInputStream(fileName);
            String suffix = fileName.substring(fileName.lastIndexOf('.') + 1);
            Iterator readers = ImageIO.getImageReadersBySuffix(suffix);
            ImageReader imageReader = (ImageReader) readers.next();
            ImageInputStream iis = ImageIO.createImageInputStream(fis);
            imageReader.setInput(iis, false);
            int num = imageReader.getNumImages(true);
            _logger.log(Level.INFO, "Images found:{0}", num);
            BufferedImage[] images = new BufferedImage[num];
            for (int i = 0; i < num; ++i)
                images[i] = imageReader.read(i);
            if(num > 0) {
                _image = images[0];
            }
            imageReader.dispose();
            _width = _image.getWidth();
            _height = _image.getHeight();
            _transparent = _image.getTransparency() != Transparency.OPAQUE;
            AttributeDialog.Instance().clearAttributes();
            Metadata metadata = ImageMetadataReader.readMetadata(f);
            Iterable<Directory> directories = metadata.getDirectories();
            Iterator<Directory> it = directories.iterator();
            while(it.hasNext()) {
                Directory d = it.next();
                AttributeDialog.Instance().addDirectory(d);
            }
            _presentor.refreshImage();
            App.Instance().fileOpened(f);
        }
        catch(ImageProcessingException e) {
            _logger.log(Level.SEVERE, "failed: {0}", e.getMessage());
        }
        catch(IOException e) {
            _logger.log(Level.SEVERE, "Failed: {0}", e.getMessage());
            _image = null;
        }
    }

    void saveAs(File f) {
        try {
            _logger.log(Level.INFO, "Save file {0}", f.getName());
            ImageIO.write(_image, "jpg", f);
        }
        catch(IOException e) {
            _logger.log(Level.SEVERE, "Failed: {0}", e.getMessage());
        }
    }

    void closeFile() {
        _logger.log(Level.INFO, "Closing image");
        clearHistory();
        _image = null;
        _width = 0;
        _height = 0;
        _presentor.repaint();
        App.Instance().fileClosed();
    }

    void backup() {
        _logger.log(Level.INFO, "Taking backup");
        int imgType = _image.getTransparency() == Transparency.OPAQUE ?
            BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
        BufferedImage backup = new BufferedImage(_image.getWidth(), _image.getHeight(), imgType);
        Graphics2D g = backup.createGraphics();
        g.drawImage(_image, 0, 0, null);
        g.dispose();
        _history.push(backup);
        App.Instance().historyChanged(_history.size());
    }

    void undo() {
        if(_history.empty()) {
            JOptionPane.showMessageDialog(App.Instance(), "No history.", "Photo Expert",
                JOptionPane.ERROR_MESSAGE);
            return;
        }
        _logger.log(Level.INFO, "Undo");
        _image = _history.pop();
        _width = _image.getWidth();
        _height = _image.getHeight();
        _presentor.refreshImage();
        App.Instance().historyChanged(_history.size());
    }

    void clearHistory() {
        _logger.log(Level.INFO, "Clear history.");
        _history.clear();
        App.Instance().historyChanged(_history.size());
    }

    void trim() {
        backup();
        _logger.log(Level.INFO, "Trim selection");
        RectSelection s = MRectangleSelector.Instance().getSelection();
        if(s == null) {
            JOptionPane.showMessageDialog(App.Instance(), "Nothing selected.", "Photo Expert",
                JOptionPane.ERROR_MESSAGE);
            return;
        }
        RectSelection.Area area = s.getImageArea();
        _image = _image.getSubimage(area._x, area._y, area._width, area._height);
        _width = _image.getWidth();
        _height = _image.getHeight();
        _presentor.refreshImage();
        _presentor.setMouseMode(Presentor.MOUSE_MODES.DRAGGING);
    }

    void resize(int targetWidth, int targetHeight, boolean higherQuality) {
        backup();
        _logger.log(Level.INFO, "Resize to width={0} height={1}",
            new Object[]{targetWidth, targetHeight});
        int type = (_image.getTransparency() == Transparency.OPAQUE) ?
            BufferedImage.TYPE_INT_RGB : BufferedImage.TYPE_INT_ARGB;
        BufferedImage newImage = (BufferedImage)_image;
        int w, h;
        if (higherQuality) {
            // Use multi-step technique: start with original size, then
            // scale down in multiple passes with drawImage()
            // until the target size is reached
            w = _image.getWidth();
            h = _image.getHeight();
        } else {
            // Use one-step technique: scale directly from original
            // size to target size with a single drawImage() call
            w = targetWidth;
            h = targetHeight;
        }
        do {
            if (higherQuality && w > targetWidth) {
                w /= 2;
                if (w < targetWidth) {
                    w = targetWidth;
                }
            }
            if (higherQuality && h > targetHeight) {
                h /= 2;
                if (h < targetHeight) {
                    h = targetHeight;
                }
            }
            BufferedImage tmp = new BufferedImage(w, h, type);
            Graphics2D g2 = tmp.createGraphics();
            g2.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);
            g2.drawImage(newImage, 0, 0, w, h, null);
            g2.dispose();
            newImage = tmp;
        } while (w != targetWidth || h != targetHeight);
        _image = newImage;
        _width = targetWidth;
        _height = targetHeight;
        _presentor.refreshImage();
        _logger.log(Level.INFO, "Finished resize");
    }

    void adjust(BufferedImageOp op) {
        _logger.log(Level.INFO, "adjust with operate={0}", op);
        backup();
        if(op instanceof ConvolveOp) {
            // workaround ConvolveOp bug 4957775 (Sun) by copying to a temporary image first
            BufferedImage img = new BufferedImage(_width, _height, BufferedImage.TYPE_INT_RGB);
            Graphics g = img.createGraphics();
            g.drawImage(_image, 0, 0, null);
            g.dispose();
            _image = op.filter(img, null);
        }
        else
            op.filter(_image, _image);
        _presentor.repaint();
    }

    void fillExpand(int x, int y, Color color) {
        if(x >= _width || y >= _height)
            return;
        backup();
        int oldRGB = _image.getRGB(x, y);
        _replacedR = (oldRGB & 0x00ff0000) >>> 16;
        _replacedG = (oldRGB & 0x0000ff00) >>> 8;
        _replacedB = oldRGB & 0x000000ff;
        _fillingRGB = color.getRGB();
        if(oldRGB == _fillingRGB) {
            _logger.log(Level.INFO, "fillExpand: oldRGB is same as newRGB!");
            return;
        }
        _logger.log(Level.INFO, "fill point {0},{1} with color {2}", new Object[]{x, y, _fillingRGB});
        _fillQueue.clear();
        _fillSet.clear();
        _fillQueue.add(new FillingPoint(x, y, true, true, true, true));
        _fillSet.add(new Point(x, y));
        while(!_fillQueue.isEmpty())
            fillPoint(_fillQueue.poll());
        _presentor.repaint();
        _logger.log(Level.INFO, "finished filling {0} points", _fillSet.size());
        _fillSet.clear();
    }

    void flipVertical() {
        _logger.log(Level.ALL, "flip vertical");
        backup();
        int imgType = _transparent ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB;
        BufferedImage newImage = new BufferedImage(_width, _height, imgType);
        Graphics g = newImage.createGraphics();
        g.drawImage(_image, 0, 0, _width - 1, _height - 1, 0, _height - 1, _width - 1, 0, null);
        g.dispose();
        _image = newImage;
        _presentor.repaint();
    }

    void flipHorizontal() {
        _logger.log(Level.ALL, "flip horizontal");
        backup();
        int imgType = _transparent ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB;
        BufferedImage newImage = new BufferedImage(_width, _height, imgType);
        Graphics g = newImage.createGraphics();
        g.drawImage(_image, 0, 0, _width - 1, _height - 1, _width - 1, 0, 0, _height - 1, null);
        g.dispose();
        _image = newImage;
        _presentor.repaint();
    }

    // clockwise -- rotate 90 degree
    // counterClockwise -- rotate -90 degree
    // the reason not to use AffineTransform for +-90 degree rotate is because it can cause small inaccuracy
    // especially when picture is small
    // the rotate dialog uses AffineTransform
    void rotate(int degree) {
        _logger.log(Level.INFO, "rotate {0}", degree);
        backup();
        if(degree == 90) {
            clockwise();
            return;
        } else if(degree == 270) {
            counterClockwise();
            return;
        }
    }

    void clockwise() {
        _logger.log(Level.INFO, "closewise rotate");
        backup();
        int imgType = _transparent ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB;
        BufferedImage newImage = new BufferedImage(_height, _width, imgType);
        for(int x = 0; x < _width; ++x)
            for(int y = 0; y < _height; ++y) {
                int rgb = _image.getRGB(x, y);
                newImage.setRGB(_height - y - 1, x, rgb);
            }
        _image = newImage;
        _width = _image.getWidth();
        _height = _image.getHeight();
        _presentor.refreshImage();
    }

    void counterClockwise() {
        _logger.log(Level.INFO, "counterClosewise rotate");
        backup();
        int imgType = _transparent ? BufferedImage.TYPE_INT_ARGB : BufferedImage.TYPE_INT_RGB;
        BufferedImage newImage = new BufferedImage(_height, _width, imgType);
        for(int x = 0; x < _width; ++x)
            for(int y = 0; y < _height; ++y) {
                int rgb = _image.getRGB(x, y);
                newImage.setRGB(y, _width - x - 1, rgb);
            }
        _image = newImage;
        _width = _image.getWidth();
        _height = _image.getHeight();
        _presentor.refreshImage();
    }

    void smoothen() {
        _logger.log(Level.INFO, "Smoothen");
        backup();
        MedianOp op = new MedianOp(1, null);
        _image = op.filter(_image, null);
        _presentor.repaint();
    }

    /* --------------------------------------------------------------------- */

    private Implementor() {
    }

    private class FillingPoint {
        FillingPoint(int x, int y, boolean north, boolean east, boolean south, boolean west) {
            _x = x;
            _y = y;
            _north = north;
            _east = east;
            _south = south;
            _west = west;
        }
        int     _x;
        int     _y;
        boolean _north;
        boolean _east;
        boolean _south;
        boolean _west;
    }

    private void fillPoint(FillingPoint p) {
        if(p._x < 0 || p._y < 0 || p._x >= _width || p._y >= _height) {
            _logger.log(Level.INFO, "FillingPoint out of range.");
        }
        int rgb = _image.getRGB(p._x, p._y);
        int r = (rgb & 0x00ff0000) >>> 16;
        int g = (rgb & 0x0000ff00) >>> 8;
        int b = (rgb & 0x000000ff);
        if(Math.abs(r - _replacedR) < 20 &&
            Math.abs(g - _replacedG) < 20 &&
            Math.abs(b - _replacedB) < 20) {
            _image.setRGB(p._x, p._y, _fillingRGB);
            Point newPoint = new Point(p._x, p._y - 1);
            if(p._north && p._y > 0 && !_fillSet.contains(newPoint)) {
                _fillQueue.add(new FillingPoint(p._x, p._y - 1, true, true, false, true));
                _fillSet.add(newPoint);
            }
            newPoint = new Point(p._x, p._y + 1);
            if(p._south && p._y < _height - 1 && !_fillSet.contains(newPoint)) {
                _fillQueue.add(new FillingPoint(p._x, p._y + 1, false, true, true, true));
                _fillSet.add(newPoint);
            }
            newPoint = new Point(p._x + 1, p._y);
            if(p._east && p._x < _width - 1 && !_fillSet.contains(newPoint)) {
                _fillQueue.add(new FillingPoint(p._x + 1, p._y, !p._north, true, !p._south, false));
                _fillSet.add(newPoint);
            }
            newPoint = new Point(p._x - 1, p._y);
            if(p._west && p._x > 0 && !_fillSet.contains(newPoint)) {
                _fillQueue.add(new FillingPoint(p._x - 1, p._y, !p._north, false, !p._south, true));
                _fillSet.add(newPoint);
            }
        }
    }

    private static Implementor      _instance = null;
    private Presentor               _presentor = null;
    private static final Logger     _logger = App._logger;
    private int                     _width = 0;
    private int                     _height = 0;
    private boolean                 _transparent = false;
    private BufferedImage           _image = null;
    private Stack<BufferedImage>    _history = new Stack<BufferedImage>();
    // fillExpand used variables
    private Queue<FillingPoint>     _fillQueue = new LinkedList<FillingPoint>();
    private HashSet<Point>          _fillSet = new HashSet<Point>();
    private int                     _fillingRGB;
    private int                     _replacedR;
    private int                     _replacedG;
    private int                     _replacedB;
}
